package entitystoragemongodb

import (
	"context"
	"errors"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"io"
	"time"

	"github.com/xiaonanln/goworld/engine/common"
	"github.com/xiaonanln/goworld/engine/gwlog"
	"github.com/xiaonanln/goworld/engine/storage/storage_common"
)

const (
	_DEFAULT_DB_NAME = "goworld"
)

var (
	db *mongo.Database
)

type mongoDBEntityStorge struct {
	db *mongo.Database
}

// OpenMongoDB opens mongodb as entity storage
func OpenMongoDB(url string, dbname string) (storagecommon.EntityStorage, error) {
	gwlog.Debugf("Connecting MongoDB ...")
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	client, err := mongo.Connect(ctx, options.Client().ApplyURI(url))
	if err != nil {
		return nil, err
	}

	if dbname == "" {
		// if db is not specified, use default
		dbname = _DEFAULT_DB_NAME
	}
	db = client.Database(dbname)
	return &mongoDBEntityStorge{
		db: db,
	}, nil
}

func (es *mongoDBEntityStorge) Write(typeName string, entityID common.EntityID, data interface{}) error {
	col := es.getCollection(typeName)
	filter := bson.M{"_id": entityID}
	update := bson.M{"$set": bson.M{"data": data}}
	opts := options.Update().SetUpsert(true)
	_, err := col.UpdateOne(context.TODO(), filter, update, opts)
	return err
}

func (es *mongoDBEntityStorge) Read(typeName string, entityID common.EntityID) (interface{}, error) {
	col := es.getCollection(typeName)
	filter := bson.M{"_id": entityID}
	var doc bson.M
	err := col.FindOne(context.TODO(), filter).Decode(&doc)
	if err != nil {
		if errors.Is(err, mongo.ErrNoDocuments) {
			return nil, mongo.ErrNoDocuments
		}
		return nil, err
	}
	return es.convertM2Map(doc["data"].(bson.M)), nil
}

func (es *mongoDBEntityStorge) convertM2Map(m bson.M) map[string]interface{} {
	ma := map[string]interface{}(m)
	es.convertM2MapInMap(ma)
	return ma
}

func (es *mongoDBEntityStorge) convertM2MapInMap(m map[string]interface{}) {
	for k, v := range m {
		switch im := v.(type) {
		case bson.M:
			m[k] = es.convertM2Map(im)
		case map[string]interface{}:
			es.convertM2MapInMap(im)
		case []interface{}:
			es.convertM2MapInList(im)
		}
	}
}

func (es *mongoDBEntityStorge) convertM2MapInList(l []interface{}) {
	for i, v := range l {
		switch im := v.(type) {
		case bson.M:
			l[i] = es.convertM2Map(im)
		case map[string]interface{}:
			es.convertM2MapInMap(im)
		case []interface{}:
			es.convertM2MapInList(im)
		}
	}
}

func (es *mongoDBEntityStorge) getCollection(typeName string) *mongo.Collection {
	return es.db.Collection(typeName)
}

func (es *mongoDBEntityStorge) List(typeName string) ([]common.EntityID, error) {
	col := es.getCollection(typeName)
	cursor, err := col.Find(context.TODO(), bson.M{}, options.Find().SetProjection(bson.M{"_id": 1}))
	if err != nil {
		return nil, err
	}
	defer cursor.Close(context.TODO())

	var entityIDs []common.EntityID
	for cursor.Next(context.TODO()) {
		var doc bson.M
		if err := cursor.Decode(&doc); err != nil {
			return nil, err
		}
		entityIDs = append(entityIDs, common.EntityID(doc["_id"].(string)))
	}
	if err := cursor.Err(); err != nil {
		return nil, err
	}
	return entityIDs, nil
}

func (es *mongoDBEntityStorge) Exists(typeName string, entityID common.EntityID) (bool, error) {
	col := es.getCollection(typeName)
	filter := bson.M{"_id": entityID}
	var doc bson.M
	err := col.FindOne(context.TODO(), filter).Decode(&doc)
	if err != nil {
		if errors.Is(err, mongo.ErrNoDocuments) {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

func (es *mongoDBEntityStorge) Close() {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	err := es.db.Client().Disconnect(ctx)
	if err != nil {
		gwlog.Errorf("Failed to disconnect from MongoDB: %v", err)
	}
}

func (es *mongoDBEntityStorge) IsEOF(err error) bool {
	return err == io.EOF || err == io.ErrUnexpectedEOF
}
